package my.util;


import java.beans.*;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Created by xnat on 17/5/18.
 */
public class PropertyUtil {

    public static void main(String[] args) {
        Object result = getProperty(new PropertyUtil(), "class.declaredMethods[1].name");
        System.out.println("result: " + result);
    }

    public static Object getProperty(Object bean, String name) {
        if (bean == null || name == null) return null;
        while (Resolver.hasNested(name)) {
            final String next = Resolver.next(name);
            Object nestedBean = null;
            if (bean instanceof Map) {
                nestedBean = getPropertyOfMapBean((Map<?, ?>) bean, next);
            } else if (Resolver.isMapped(next)) {
                nestedBean = getMappedProperty(bean, next);
            } else if (Resolver.isIndexed(next)) {
                nestedBean = getIndexedProperty(bean, next);
            } else {
                nestedBean = getSimpleProperty(bean, next);
            }
            if (nestedBean == null) {
                return null;
            }
            bean = nestedBean;
            name = Resolver.remove(name);
        }
        if (bean instanceof Map) {
            bean = getPropertyOfMapBean((Map<?, ?>) bean, name);
        } else if (Resolver.isMapped(name)) {
            bean = getMappedProperty(bean, name);
        } else if (Resolver.isIndexed(name)) {
            bean = getIndexedProperty(bean, name);
        } else {
            bean = getSimpleProperty(bean, name);
        }
        return bean;
    }

    public static void setProperty(Object bean, String propName, final Object value) {
        if (bean == null) {
            throw new IllegalArgumentException("No bean specified");
        }
        if (propName == null) {
            throw new IllegalArgumentException("propertyName must not be null");
        }

        // Resolve nested references
        while (Resolver.hasNested(propName)) {
            final String next = Resolver.next(propName);
            Object nestedBean = null;
            if (bean instanceof Map) {
                nestedBean = getPropertyOfMapBean((Map<?, ?>)bean, next);
            } else if (Resolver.isMapped(next)) {
                nestedBean = getMappedProperty(bean, next);
            } else if (Resolver.isIndexed(next)) {
                nestedBean = getIndexedProperty(bean, next);
            } else {
                nestedBean = getSimpleProperty(bean, next);
            }
            if (nestedBean == null) {
                throw new RuntimeException("Null property value for '" + propName + "' on bean class '" + bean.getClass() + "'");
            }
            bean = nestedBean;
            propName = Resolver.remove(propName);
        }

        try {
            if (bean instanceof Map) {
                setPropertyOfMapBean((Map<String, Object>) bean, propName, value);
            } else if (Resolver.isMapped(propName)) {
                setMappedProperty(bean, propName, value);
            } else if (Resolver.isIndexed(propName)) {
                setIndexedProperty(bean, propName, value);
            } else {
                setSimpleProperty(bean, propName, value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void populate(final Object bean, final Map<String, ? extends Object> properties) {

        // Do nothing unless both arguments have been specified
        if ((bean == null) || (properties == null)) {
            return;
        }

        // Loop through the property name/value pairs to be set
        for(final Map.Entry<String, ? extends Object> entry : properties.entrySet()) {
            // Identify the property name and value(s) to be assigned
            final String name = entry.getKey();
            if (name == null) {
                continue;
            }

            // Perform the assignment for this property
            setProperty(bean, name, entry.getValue());

        }

    }


    private static void setMappedProperty(final Object bean, String name, final Object value) throws NoSuchMethodException {
        if (name == null) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }

        // Identify the key of the requested individual property
        String key  = null;
        try {
            key = Resolver.getKey(name);
        } catch (final IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid mapped property '" + name +
                            "' on bean class '" + bean.getClass() + "'");
        }
        if (key == null) {
            throw new IllegalArgumentException("Invalid mapped property '" + name +
                            "' on bean class '" + bean.getClass() + "'");
        }

        // Isolate the name
        name = Resolver.getProperty(name);

        // Request the specified indexed property value
        // Retrieve the property descriptor for the specified property
        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" + name + "' on bean class '" + bean.getClass() + "'");
        }

          /* means that the result has to be retrieved from a map */
        final Method readMethod = descriptor.getReadMethod();
        if (readMethod == null) {
            throw new NoSuchMethodException("Property '" + name +
                    "' has no mapped getter method on bean class '" +
                    bean.getClass() + "'");
        }
        final Object invokeResult;
        try {
            invokeResult = readMethod.invoke(bean);
            /* test and fetch from the map */
            if (invokeResult instanceof java.util.Map) {
                final java.util.Map<String, Object> map = (Map<String, Object>) invokeResult;
                map.put(key, value);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * Set the value of the specified indexed property of the specified
     * bean, with no type conversions.  The zero-relative index of the
     * required value must be included (in square brackets) as a suffix to
     * the property name, or <code>IllegalArgumentException</code> will be
     * thrown.  In addition to supporting the JavaBeans specification, this
     * method has been extended to support <code>List</code> objects as well.
     *
     * @param bean Bean whose property is to be modified
     * @param name <code>propertyname[index]</code> of the property value
     *  to be modified
     * @param value Value to which the specified property element
     *  should be set
     *
     * @throws IndexOutOfBoundsException if the specified index
     *  is outside the valid range for the underlying property
     * @throws IllegalAccessException if the caller does not have
     *  access to the property accessor method
     * @throws IllegalArgumentException if <code>bean</code> or
     *  <code>name</code> is null
     * @throws InvocationTargetException if the property accessor method
     *  throws an exception
     * @throws NoSuchMethodException if an accessor method for this
     *  propety cannot be found
     */
    private static void setIndexedProperty(final Object bean, String name, final Object value) throws NoSuchMethodException {

        if (name == null) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }

        // Identify the index of the requested individual property
        int index = -1;
        try {
            index = Resolver.getIndex(name);
        } catch (final IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid indexed property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        }
        if (index < 0) {
            throw new IllegalArgumentException("Invalid indexed property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        }

        // Isolate the name
        name = Resolver.getProperty(name);

        // Set the specified indexed property value
        if (name == null || name.length() == 0) {
            if (bean.getClass().isArray()) {
                Array.set(bean, index, value);
                return;
            } else if (bean instanceof List) {
                final List<Object> list = (List<Object>) bean;
                list.set(index, value);
                return;
            }
        }
        if (name == null) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }


        // Retrieve the property descriptor for the specified property
        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on bean class '" + bean.getClass() + "'");
        }

        // Call the indexed setter method if there is one
        if (descriptor instanceof IndexedPropertyDescriptor) {
            Method writeMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedWriteMethod();
            if (writeMethod != null) {

                final Object[] subscript = new Object[2];
                subscript[0] = new Integer(index);
                subscript[1] = value;
                try {
                    writeMethod.setAccessible(true);
                    writeMethod.invoke(bean, subscript);
                } catch (final Exception e) {
                    e.printStackTrace();
                }
                return;
            }
        }

        // Otherwise, the underlying property must be an array or a list
        final Method readMethod = descriptor.getReadMethod();
        if (readMethod == null) {
            throw new NoSuchMethodException("Property '" + name +
                    "' has no getter method on bean class '" + bean.getClass() + "'");
        }

        // Call the property getter to get the array or list

        try {
            final Object v = readMethod.invoke(bean);
            if (v.getClass().isArray()) {
                // Modify the specified value in the array
                Array.set(v, index, value);

            } else if (v instanceof List) {
                // Modify the specified value in the List
                final List<Object> list = (List<Object>) v;
                list.set(index, value);
            } else {
                throw new IllegalArgumentException("Property '" + name +
                        "' is not indexed on bean class '" + bean.getClass() + "'");
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * Set the value of the specified simple property of the specified bean,
     * with no type conversions.
     *
     * @param bean Bean whose property is to be modified
     * @param name Name of the property to be modified
     * @param value Value to which the property should be set
     *
     * @throws IllegalAccessException if the caller does not have
     *  access to the property accessor method
     * @throws IllegalArgumentException if <code>bean</code> or
     *  <code>name</code> is null
     * @throws IllegalArgumentException if the property name is
     *  nested or indexed
     * @throws InvocationTargetException if the property accessor method
     *  throws an exception
     * @throws NoSuchMethodException if an accessor method for this
     *  propety cannot be found
     */
    private static void setSimpleProperty(final Object bean, final String name, final Object value) throws NoSuchMethodException {

        if (name == null) {
            throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'");
        }

        // Retrieve the property setter method for the specified property
        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on class '" + bean.getClass() + "'" );
        }
        final Method writeMethod = descriptor.getWriteMethod();
        if (writeMethod == null) {
            throw new NoSuchMethodException("Property '" + name +
                    "' has no setter method in class '" + bean.getClass() + "'");
        }
        try {
            writeMethod.setAccessible(true);
            writeMethod.invoke(bean, value);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }


    private static void setPropertyOfMapBean(final Map<String, Object> bean, String propertyName, final Object value) {
        if (Resolver.isMapped(propertyName)) {
            final String name = Resolver.getProperty(propertyName);
            if (name == null || name.length() == 0) {
                propertyName = Resolver.getKey(propertyName);
            }
        }

        if (Resolver.isIndexed(propertyName) ||
                Resolver.isMapped(propertyName)) {
            throw new IllegalArgumentException(
                    "Indexed or mapped properties are not supported on"
                            + " objects of type Map: " + propertyName);
        }

        bean.put(propertyName, value);
    }

    private static Object getMappedProperty(final Object bean, String name) {
        String key = Resolver.getKey(name);
        name = Resolver.getProperty(name);
        Object mapValue = getSimpleProperty(bean, name);
        if (mapValue instanceof Map) {
            return ((Map) mapValue).get(key);
        }
        return null;
    }

    private static Object getIndexedProperty(final Object bean, String name) {
        int index = Resolver.getIndex(name);
        name = Resolver.getProperty(name);
        final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
        if (descriptor instanceof IndexedPropertyDescriptor) {
            Method readMethod = ((IndexedPropertyDescriptor) descriptor).getIndexedReadMethod();
            if (readMethod != null) {
                final Object[] subscript = new Object[1];
                subscript[0] = new Integer(index);
                try {
                    return readMethod.invoke(bean, subscript);
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            }
        }
        Object indexedValue = getSimpleProperty(bean, name);
        if (indexedValue != null) {
            Class<? extends Object> cls = indexedValue.getClass();
            if (cls.isArray()) {
                return Array.get(indexedValue, index);
            } else if (List.class.isAssignableFrom(cls)) {
                return ((List) indexedValue).get(index);
            }
        }
        return null;
    }

    private static Object getPropertyOfMapBean(final Map<?, ?> bean, String propertyName) {
        if (Resolver.isMapped(propertyName)) {
            final String name = Resolver.getProperty(propertyName);
            if (name == null || name.length() == 0) {
                propertyName = Resolver.getKey(propertyName);
            }
        }
        return bean.get(propertyName);
    }

    private static Object getSimpleProperty(final Object bean, final String name) {
        PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
        Method method = descriptor.getReadMethod();
        method.setAccessible(true);
        try {
            Object ret = method.invoke(bean);
            return ret;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static PropertyDescriptor getPropertyDescriptor(Object bean, String name) {
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
            for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
                if (Objects.equals(name, pd.getName())) return pd;
            }
        } catch (IntrospectionException e) {
            e.printStackTrace();
        }
        return null;
    }


    static class Resolver {
        private static final char NESTED        = '.';
        private static final char MAPPED_START  = '(';
        private static final char MAPPED_END    = ')';
        private static final char INDEXED_START = '[';
        private static final char INDEXED_END   = ']';

        public Resolver() {
        }

        /**
         * Return the index value from the property expression or -1.
         *
         * @param expression The property expression
         * @return The index value or -1 if the property is not indexed
         * @throws IllegalArgumentException If the indexed property is illegally
         *                                  formed or has an invalid (non-numeric) value.
         */
        public static int getIndex(final String expression) {
            if (expression == null || expression.length() == 0) {
                return -1;
            }
            for (int i = 0; i < expression.length(); i++) {
                final char c = expression.charAt(i);
                if (c == NESTED || c == MAPPED_START) {
                    return -1;
                } else if (c == INDEXED_START) {
                    final int end = expression.indexOf(INDEXED_END, i);
                    if (end < 0) {
                        throw new IllegalArgumentException("Missing End Delimiter");
                    }
                    final String value = expression.substring(i + 1, end);
                    if (value.length() == 0) {
                        throw new IllegalArgumentException("No Index Value");
                    }
                    int index = 0;
                    try {
                        index = Integer.parseInt(value, 10);
                    } catch (final Exception e) {
                        throw new IllegalArgumentException("Invalid index value '"
                                + value + "'");
                    }
                    return index;
                }
            }
            return -1;
        }

        /**
         * Return the map key from the property expression or <code>null</code>.
         *
         * @param expression The property expression
         * @return The index value
         * @throws IllegalArgumentException If the mapped property is illegally formed.
         */
        public static String getKey(final String expression) {
            if (expression == null || expression.length() == 0) {
                return null;
            }
            for (int i = 0; i < expression.length(); i++) {
                final char c = expression.charAt(i);
                if (c == NESTED || c == INDEXED_START) {
                    return null;
                } else if (c == MAPPED_START) {
                    final int end = expression.indexOf(MAPPED_END, i);
                    if (end < 0) {
                        throw new IllegalArgumentException("Missing End Delimiter");
                    }
                    return expression.substring(i + 1, end);
                }
            }
            return null;
        }

        /**
         * Return the property name from the property expression.
         *
         * @param expression The property expression
         * @return The property name
         */
        public static String getProperty(final String expression) {
            if (expression == null || expression.length() == 0) {
                return expression;
            }
            for (int i = 0; i < expression.length(); i++) {
                final char c = expression.charAt(i);
                if (c == NESTED) {
                    return expression.substring(0, i);
                } else if (c == MAPPED_START || c == INDEXED_START) {
                    return expression.substring(0, i);
                }
            }
            return expression;
        }

        /**
         * Indicates whether or not the expression
         * contains nested property expressions or not.
         *
         * @param expression The property expression
         * @return The next property expression
         */
        public static boolean hasNested(final String expression) {
            if (expression == null || expression.length() == 0) {
                return false;
            } else {
                return (remove(expression) != null);
            }
        }

        /**
         * Indicate whether the expression is for an indexed property or not.
         *
         * @param expression The property expression
         * @return <code>true</code> if the expresion is indexed,
         * otherwise <code>false</code>
         */
        public static boolean isIndexed(final String expression) {
            if (expression == null || expression.length() == 0) {
                return false;
            }
            for (int i = 0; i < expression.length(); i++) {
                final char c = expression.charAt(i);
                if (c == NESTED || c == MAPPED_START) {
                    return false;
                } else if (c == INDEXED_START) {
                    return true;
                }
            }
            return false;
        }

        /**
         * Indicate whether the expression is for a mapped property or not.
         *
         * @param expression The property expression
         * @return <code>true</code> if the expresion is mapped,
         * otherwise <code>false</code>
         */
        public static boolean isMapped(final String expression) {
            if (expression == null || expression.length() == 0) {
                return false;
            }
            for (int i = 0; i < expression.length(); i++) {
                final char c = expression.charAt(i);
                if (c == NESTED || c == INDEXED_START) {
                    return false;
                } else if (c == MAPPED_START) {
                    return true;
                }
            }
            return false;
        }

        /**
         * Extract the next property expression from the
         * current expression.
         *
         * @param expression The property expression
         * @return The next property expression
         */
        public static String next(final String expression) {
            if (expression == null || expression.length() == 0) {
                return null;
            }
            boolean indexed = false;
            boolean mapped = false;
            for (int i = 0; i < expression.length(); i++) {
                final char c = expression.charAt(i);
                if (indexed) {
                    if (c == INDEXED_END) {
                        return expression.substring(0, i + 1);
                    }
                } else if (mapped) {
                    if (c == MAPPED_END) {
                        return expression.substring(0, i + 1);
                    }
                } else {
                    if (c == NESTED) {
                        return expression.substring(0, i);
                    } else if (c == MAPPED_START) {
                        mapped = true;
                    } else if (c == INDEXED_START) {
                        indexed = true;
                    }
                }
            }
            return expression;
        }

        /**
         * Remove the last property expresson from the
         * current expression.
         *
         * @param expression The property expression
         * @return The new expression value, with first property
         * expression removed - null if there are no more expressions
         */
        public static String remove(final String expression) {
            if (expression == null || expression.length() == 0) {
                return null;
            }
            final String property = next(expression);
            if (expression.length() == property.length()) {
                return null;
            }
            int start = property.length();
            if (expression.charAt(start) == NESTED) {
                start++;
            }
            return expression.substring(start);
        }
    }
}
